<html>
 <head>
  <meta charset="UTF-8">
 </head>
 <body>
  <h1 data-lake-id="emJk5" id="emJk5"><span data-lake-id="u25dfcb36" id="u25dfcb36">典型回答</span></h1>
  <p data-lake-id="u8e14ffe4" id="u8e14ffe4"><br></p>
  <p data-lake-id="u80bb6eb7" id="u80bb6eb7"><span data-lake-id="u5ed659dd" id="u5ed659dd">首先，我们需要问清楚，一天100W次的登录，在一天内有没有某个时段是高峰的？高峰期的QPS大概可以达到多少。</span></p>
  <p data-lake-id="u6455fbd7" id="u6455fbd7"><span data-lake-id="uc370df92" id="uc370df92">​</span><br></p>
  <p data-lake-id="u16f7f64c" id="u16f7f64c"><span data-lake-id="u793b715e" id="u793b715e">如果没有高峰期，虽然100万听上去挺多的，但是其实平均下来一秒钟的QPS也就10，这个量的话，其实根本不需要做什么特别的JVM优化。</span></p>
  <p data-lake-id="uc2a53444" id="uc2a53444"><span data-lake-id="ucf517097" id="ucf517097">​</span><br></p>
  <p data-lake-id="u35508b9d" id="u35508b9d"><span data-lake-id="ua1e57821" id="ua1e57821">一般业务场景中，都是有自己的业务高峰期的，比如电商业务基本上上午十点和下午两点是业务高峰期，基本上这时候的QPS是平时的20倍都不止。</span></p>
  <p data-lake-id="ucf6dae25" id="ucf6dae25"><span data-lake-id="u837e183b" id="u837e183b">​</span><br></p>
  <p data-lake-id="u4abfcb47" id="u4abfcb47"><u><span data-lake-id="u47b9b9a5" id="u47b9b9a5">我们假设登录业务存在高峰期，峰值时长大概持续1个小时，峰值的QPS可以达到2000。那么需要做哪些优化？</span></u></p>
  <p data-lake-id="u66007c36" id="u66007c36"><u><span data-lake-id="ue72ca519" id="ue72ca519">​</span></u><br></p>
  <blockquote data-lake-id="ub66fa833" id="ub66fa833">
   <p data-lake-id="u97191b85" id="u97191b85"><span data-lake-id="u37dea4e9" id="u37dea4e9">作为一个登录服务，一般来说我们在接收到请求之后，只需要给用户进行鉴权并把结果返回给前端就行了。在这个过程中一般不太会去查询太多的数据，比如权限什么的也都是在后面访问页面再查询的。所以，峰值2000左右的QPS，对于JVM的内存来说，最主要的就是会因为远程调用，而创建出很多请求参数和请求的响应。而这些对象基本都是朝生暮死的，接口调用结束之后就会被回收掉。并且通常来说这些对象也不会很大， 因为登录并不是注册，其实并不携带特别多的信息，那么也就是说，会产生大量的小对象，即新生代会不断的创建对象，并被回收掉。</span></p>
  </blockquote>
  <p data-lake-id="u5da04de9" id="u5da04de9"><span data-lake-id="u9db922fb" id="u9db922fb">​</span><br></p>
  <p data-lake-id="u0aa1b12a" id="u0aa1b12a"><span data-lake-id="u6bce3ab3" id="u6bce3ab3">基于以上简单分析，我们看下该如何做我们的JVM调优。</span></p>
  <h4 data-lake-id="rhq9w" id="rhq9w"><span data-lake-id="u16d295e7" id="u16d295e7">堆内存设置</span></h4>
  <p data-lake-id="ud45a665d" id="ud45a665d"><br></p>
  <p data-lake-id="u75745ccf" id="u75745ccf"><span data-lake-id="ucebd0be6" id="ucebd0be6">首先是堆内存大小的设置。当我们的机器只有4核8G的时候，堆内存的大小肯定不能太大，一般不建议设置的太大，因为我们需要给机器上的其他应用预留出一部分内容。所以，</span><strong><span data-lake-id="u4f0f0c83" id="u4f0f0c83">我们一般建议都是把JVM的堆内存设置成操作系统内存的一半，也就是4G。</span></strong><span data-lake-id="u0f83d3ac" id="u0f83d3ac">至于初始内存和最大内存，我们这场景中建议设置成一样的。这样可以避免 JVM 在运行过程中频繁进行内存扩容和收缩操作，提高应用程序的性能和稳定性。</span><strong><span data-lake-id="u79b15101" id="u79b15101">即：</span></strong></p>
  <p data-lake-id="u442bad29" id="u442bad29"><strong><span data-lake-id="u6860730f" id="u6860730f">​</span></strong><br></p>
  <pre lang="java"><code>
-Xms4G -Xmx4G
</code></pre>
  <p data-lake-id="u32a616a7" id="u32a616a7"><strong><span data-lake-id="u3f33d07e" id="u3f33d07e">​</span></strong><br></p>
  <h4 data-lake-id="XvWUE" id="XvWUE"><span data-lake-id="ued290b52" id="ued290b52">垃圾收集器选择</span></h4>
  <p data-lake-id="ud9925b31" id="ud9925b31"><br></p>
  <p data-lake-id="ue169e60b" id="ue169e60b"><span data-lake-id="u8d5bb6a4" id="u8d5bb6a4">在设置了堆空间的总大小之后，我们需要考虑用那种垃圾收集器。另外，我们前面分析过，这个业务中会频繁在新生代创建并销毁对象，那么，就意味着新生代的GC会比较频繁。所以我们需要选择一种在GC过程中STW时间短的，并且在年轻代的回收中也能发挥效果的。</span></p>
  <p data-lake-id="u3dec8806" id="u3dec8806"><span data-lake-id="u0dd78c66" id="u0dd78c66">​</span><br></p>
  <p data-lake-id="ufbcfafcb" id="ufbcfafcb"><span data-lake-id="u72535b74" id="u72535b74">在新生代的垃圾收集器中，主要以Serial、ParNew、Parallel Scavenge以及支持整堆回收的G1了。</span></p>
  <p data-lake-id="ub93876e1" id="ub93876e1"><span data-lake-id="u72e0b32c" id="u72e0b32c">​</span><br></p>
  <p data-lake-id="u976a806b" id="u976a806b"><span data-lake-id="u65bcf752" id="u65bcf752">因为新生代采用的都是复制算法，所以不太需要考虑碎片的问题，我们主要考虑吞吐量和STW的时长就行了。</span></p>
  <p data-lake-id="u9d04b3dc" id="u9d04b3dc"><span data-lake-id="u5eac38dc" id="u5eac38dc">​</span><br></p>
  <p data-lake-id="uce9e2ce4" id="uce9e2ce4"><span data-lake-id="ua5035dd8" id="ua5035dd8">首先排除单线程的Serial，剩下ParNew是一个并发的收集器，Parallel Scavenge更加关注吞吐量，而G1作为JDK 9中默认垃圾收集器，他不仅同时具有低暂停时间和高吞吐量的优点，但是他对内存有要求，最小要4G，</span></p>
  <p data-lake-id="u8be897b6" id="u8be897b6"><span data-lake-id="u34ee9088" id="u34ee9088">​</span><br></p>
  <p data-lake-id="u3f24c5a0" id="u3f24c5a0"><span data-lake-id="u7f816105" id="u7f816105">从使用门槛上来说，G1是可以用的，因为一般来说，内存要大于等于4G的话，才适合使用G1进行GC。</span></p>
  <p data-lake-id="u17563714" id="u17563714"><span data-lake-id="u80eaca7c" id="u80eaca7c">​</span><br></p>
  <p data-lake-id="u03a110bb" id="u03a110bb"><span data-lake-id="ua5cd0d6f" id="ua5cd0d6f">所以，我们采用G1作为垃圾收集器：</span></p>
  <p data-lake-id="ub528f630" id="ub528f630"><span data-lake-id="uba0f0165" id="uba0f0165">​</span><br></p>
  <pre lang="java"><code>
-XX：+UseG1GC
</code></pre>
  <p data-lake-id="uaa31ecc1" id="uaa31ecc1"><br></p>
  <p data-lake-id="u5d945a05" id="u5d945a05"><br></p>
  <p data-lake-id="ud4a8eafd" id="ud4a8eafd"><span data-lake-id="ue76ab275" id="ue76ab275">在使用了G1之后，其实他自己是有一套自动的预测和调优机制的。我们只需要通过</span><code data-lake-id="u6d8e65d1" id="u6d8e65d1"><span data-lake-id="u8dd60dca" id="u8dd60dca">-XX:MaxGCPauseMillis</span></code><span data-lake-id="uc9fc2ec4" id="uc9fc2ec4">参数来设置最大停顿时间就行了。一般建议设置到100-200之间，一般这个时长对用户来说基本无感知：</span></p>
  <p data-lake-id="ufab828f8" id="ufab828f8"><span data-lake-id="u5ef4b140" id="u5ef4b140">​</span><br></p>
  <pre lang="java"><code>
XX:MaxGCPauseMillis=200
</code></pre>
  <p data-lake-id="ub67d6ffc" id="ub67d6ffc"><br></p>
  <p data-lake-id="ud5649e86" id="ud5649e86"><span data-lake-id="u4a9d40c6" id="u4a9d40c6">其次，我们还可以自己调节一些G1的配置，比如设置他的GC线程数，可以先配置4个线程数进行GC，后续根据实际情况再做调整：</span></p>
  <p data-lake-id="uf68c4ac3" id="uf68c4ac3"><span data-lake-id="ue4b39217" id="ue4b39217">​</span><br></p>
  <pre lang="java"><code>
-XX:ParallelGCThreads=4       // 设置并行 GC 线程数为 4
-XX:ConcGCThreads=2           // 设置并发 GC 线程数为 2
</code></pre>
  <p data-lake-id="ub65e1834" id="ub65e1834"><br></p>
  <p data-lake-id="ufc52c06d" id="ufc52c06d"><br></p>
  <h4 data-lake-id="AGBXq" id="AGBXq"><span data-lake-id="u1cab97ef" id="u1cab97ef">各区大小设置</span></h4>
  <p data-lake-id="u618bade9" id="u618bade9"><br></p>
  <p data-lake-id="uea9decd9" id="uea9decd9"><span data-lake-id="ue2f1a603" id="ue2f1a603">G1的内存划分是自适应的，它会根据堆的大小和使用情况来动态调整各个区域的大小和比例。但是，我们也可以通过一些JVM参数来手动设置G1的各个分代内存配置。</span></p>
  <p data-lake-id="u423847a8" id="u423847a8"><span data-lake-id="u1dd577e3" id="u1dd577e3">​</span><br></p>
  <p data-lake-id="u8a6f443b" id="u8a6f443b"><span data-lake-id="u67ee418b" id="u67ee418b">G1 中的分代和其他垃圾回收器不太一样，它不是严格按照年轻代和老年代划分的，而是通过划分各个区域的存活对象数量来实现垃圾回收的。因此，G1 中不需要像其他垃圾回收器那样设置新生代和老年代的大小比例，而是需要设置一些区域的内存配置。</span></p>
  <p data-lake-id="u1a5c9725" id="u1a5c9725"><span data-lake-id="uadd752e0" id="uadd752e0">​</span><br></p>
  <p data-lake-id="uf4702b84" id="uf4702b84"><code data-lake-id="u7603e384" id="u7603e384"><span data-lake-id="ude76deca" id="ude76deca">-XX:G1NewSizePercent</span></code><span data-lake-id="u94e4d28b" id="u94e4d28b"> 和 </span><code data-lake-id="u811d2b30" id="u811d2b30"><span data-lake-id="uf3163901" id="uf3163901">-XX:G1MaxNewSizePercent</span></code><span data-lake-id="ub57552cd" id="ub57552cd">分别用于设置年轻代的初始大小和最大大小，它们的默认值分别为 5% 和 60%。针对我们的业务场景，我们其实可以适当的调高一下年轻代的初始大小，5%的比例太小了，我们可以调整到30%。</span></p>
  <p data-lake-id="u95285b0d" id="u95285b0d"><br></p>
  <pre lang="java"><code>
-XX:G1HeapRegionSize=2m：将 G1 的区域大小设置为 2MB，以提高垃圾回收的效率和精度。
-XX:G1NewSizePercent=20：设置年轻代的初始大小为堆的 20%。
-XX:G1MaxNewSizePercent=50：设置年轻代的最大大小为堆的 50%。
-XX:G1OldCSetRegionThresholdPercent=10：设置老年代的大小为堆的 10%。
-XX:G1HeapWastePercent=5：设置垃圾回收后留下的未使用区域的最大比例为 5%。
</code></pre>
  <h4 data-lake-id="DbQWc" id="DbQWc"><br></h4>
  <h4 data-lake-id="OawZA" id="OawZA"><span data-lake-id="uc4e5a3bf" id="uc4e5a3bf">添加必要的日志</span></h4>
  <p data-lake-id="ufdb651df" id="ufdb651df"><br></p>
  <p data-lake-id="u616b97b4" id="u616b97b4"><span data-lake-id="u34ea023b" id="u34ea023b">因为以上配置都是根据业务大致分析出来的初始配置，所以我们一定是需要不断地调优的，那么必要的日志相关参数就要添加。如：</span></p>
  <p data-lake-id="u22b5bfce" id="u22b5bfce"><span data-lake-id="u19ab0c0a" id="u19ab0c0a">​</span><br></p>
  <pre lang="java"><code>
-XX:MaxGCPauseMillis=100：最大 GC 暂停时间为 100 毫秒，可以根据实际情况调整；
-XX:+HeapDumpOnOutOfMemoryError：当出现内存溢出时，自动生成堆内存快照文件；
-XX:HeapDumpPath=/path/to/heap/dump/file.hprof：堆内存快照文件的存储路径；
-XX:+PrintGC：输出 GC 信息；
-XX:+PrintGCDateStamps：输出 GC 发生时间；
-XX:+PrintGCTimeStamps：输出 GC 发生时 JVM 的运行时间；
-XX:+PrintGCDetails：输出 GC 的详细信息；
-Xlog:gc*:file=/path/to/gc.log:time,uptime:filecount=10,filesize=100M：将 GC 日志输出到指定文件中，可以根据需要调整日志文件路径、数量和大小
</code></pre>
  <p data-lake-id="u469c8a5f" id="u469c8a5f"><span data-lake-id="u25d8e0b0" id="u25d8e0b0">​</span><br></p>
  <p data-lake-id="u40d287d5" id="u40d287d5"><span data-lake-id="u9559a232" id="u9559a232">​</span><br></p>
  <p data-lake-id="ua1767beb" id="ua1767beb"><span data-lake-id="u4849ab15" id="u4849ab15">​</span><br></p>
  <p data-lake-id="uedd69ca3" id="uedd69ca3"><strong><span data-lake-id="u47888880" id="u47888880">​</span></strong><br></p>
  <p data-lake-id="u5325c8ab" id="u5325c8ab"><span data-lake-id="uf83da58e" id="uf83da58e">​</span><br></p>
  <p data-lake-id="uf560bbf7" id="uf560bbf7"><span data-lake-id="u644cf7f7" id="u644cf7f7">​</span><br></p>
  <p data-lake-id="u6b726799" id="u6b726799"><br></p>
  <p data-lake-id="u044ac30f" id="u044ac30f"><br></p>
 </body>
</html>